/**
 * Copyright 2012 ibiblio
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.ibiblio.terasaur.bt

import org.apache.commons.codec.binary.Hex
import org.ibiblio.terasaur.bencode.ByteArray

/**
* Represents a torrent info dictionary.  Not intended to be used as an
* independent class.  See TorrentMetainfo.
*/
class TorrentMetainfoInfoDictionary {
    public String name // filename (single file mode) or directory (multi file mode)
    public Long piece_length // bytes per piece
    public ByteArray pieces // 20 byte sha-1 hash for each piece, smushed together in a byte array
    public boolean is_private // optional, note: key is 'private', but cannot use private

    // Track whether the original input Map contains the private key.  Used
    // for torrent reencoding.
    private boolean _showPrivate
    private Map _extras // non-standard file information

    // single file mode
    public Long length
    public String md5sum // optional

    // multi file mode
    public List<TorrentMetainfoFileinfo> files = null

    /**
     * Input Map should be the info dictionary of a decoded torrent file.  If
     * keepPieces is true, make an internal copy of the piece hash array.
     * Otherwise, the pieces array is ignored.
     *
     * @param infoData
     * @param keepPieces
     */
    public TorrentMetainfoInfoDictionary(Map infoData, boolean keepPieces=false) {
        this._validate(infoData)

        this.name = infoData.get('name')
        this.piece_length = infoData.get('piece length')
        if (keepPieces) {
            this.pieces = infoData.get('pieces')
        }

        if (infoData.containsKey('private')) {
            _showPrivate = true
            if (infoData.get('private') == 1) {
                this.is_private = true
            } else {
                this.is_private = false
            }
        } else {
            _showPrivate = false
        }

        if (infoData.containsKey('length')) {
            this.length = infoData.get('length')
        }

        if (infoData.containsKey('md5sum')) {
            this.md5sum = infoData.get('md5sum')
        }

        if (infoData.containsKey('files')) {
            this.files = []
            infoData.get('files').each { this.files << new TorrentMetainfoFileinfo(it) }
        }

        _saveExtraElements(infoData)
    }

    private _saveExtraElements(Map infoData) {
        def keys = infoData.keySet() as List
        keys.removeAll(['name', 'piece length', 'pieces', 'private', 'length', 'md5sum', 'files'])
        if (keys.size() > 0) {
            _extras = [:]
            keys.each {key->
                _extras[key] = infoData.get(key)
            }
        }
    }

    private void _validate(Map infoData) {
        if (!infoData.containsKey('name')) {
            throw new Exception('Missing name metainfo info key')
        }
        if (!infoData.containsKey('piece length')) {
            throw new Exception('Missing piece length metainfo info key')
        }
        if (!infoData.containsKey('pieces')) {
            throw new Exception('Missing pieces metainfo info key')
        }
    }

    public String toString() {
        def s = "    TorrentMetainfoInfoDictionary [\n"
        s += '        name: ' + this.name + "\n"
        s += '        piece_length: ' + this.piece_length + "\n"
        if (this.pieces) {
            s += '        pieces (len: ' + this.pieces.length + ")\n"
        }
        /*
        s += '        pieces [' + this.piece_length
        this.pieces.each {
            s += '            piece: ' + Hex.encodeHex(it)
        }
        s += '        ]'
        */
        this.files.each { it.toString() }
        s += '        is_private: ' + this.is_private + "\n"
        s += '        length: ' + this.length + "\n"
        s += '        md5sum: ' + this.md5sum + "\n"
        s += "    ]\n"
        return s
    }

    public void dumpPieces() {
        println 'Torrent pieces data:'
        if (this.pieces) {
            int idx = 0
            byte[] bytes = this.pieces.getArray()
            while (idx < this.pieces.length) {
                if (idx+20 >= this.pieces.length) {
                    println '\t' + bytes[idx..this.pieces.length-1]
                } else {
                    println '\t' + bytes[idx..idx+20]
                }
                idx += 20
            }
        } else {
            println '\t[empty]'
        }
    }

    /**
     * Return info dictionary data as a Map
     *
     * @return
     */
    public Map toMap() {
        Map data = [
            'name': new ByteArray(this.name.bytes),
            'piece length': this.piece_length,
            'pieces': this.pieces
            ]
        if (this.is_private) {
            data['private'] = 1L
        } else if (_showPrivate) {
            data['private'] = 0L
        }
        if (this.length) {
            data.length = this.length
        }
        if (this.md5sum) {
            data.md5sum = this.md5sum
        }
        if (this.files) {
            data.files = []
            this.files.each { data.files << it.toMap() }
        }
        if (_extras) {
            _extras.each {key, value->
                if (value instanceof String) {
                    data[key] = new ByteArray(value.bytes)
                } else {
                    data[key] = value
                }
            }
        }
        return data
    }
}
